Conversation
Define 9 new commands under `temporal activity` for standalone (top-level) Activity Executions: start, execute, describe, list, count, cancel, terminate, delete, result. Each mirrors the corresponding `temporal workflow` command pattern. Modify `complete` and `fail` to make --workflow-id optional so they work for both workflow-scoped and standalone Activities. Add two new reusable option sets: `activity-execution-reference` (activity-id + run-id) and `activity-start` (full set of start options including timeouts, retry policy, ID policies, search attributes, headers, metadata, and priority). All new commands are marked Experimental. Existing workflow-only commands (pause, unpause, reset, update-options) are unchanged. Co-authored-by: Cursor <cursoragent@cursor.com>
- cancel: Rewrite description following Python SDK style, explaining that cancellation is a request delivered via heartbeat response - complete/fail: Use activity-reference option set instead of inlining activity-id/run-id, fixing missing short flag and missing "latest run" description - fail: Clarify detail vs reason option descriptions (detail is the failure details payload; reason is the failure message) - count/list: Add example queries, improve query option descriptions, add visibility docs links - delete: Add note about async deletion and running activity termination (from proto docs) - execute: Use "output" instead of "printed" - update-options/pause/unpause/reset: Add "Not supported for standalone Activities" sentence - Remove resolved TODO comments Co-authored-by: Cursor <cursoragent@cursor.com>
Phase 2: Run code generation producing command structs for all 9 new commands and 2 new option sets (ActivityReferenceOptions, ActivityStartOptions). Phase 3: Implement run() methods for all new commands: - start: calls StartActivityExecution, outputs activity ID and run ID - execute: calls StartActivityExecution + PollActivityExecution, outputs the activity result - describe: calls DescribeActivityExecution with include_input and include_outcome - list: calls ListActivityExecutions with pagination, table output - count: calls CountActivityExecutions with group support - cancel: calls RequestCancelActivityExecution - terminate: calls TerminateActivityExecution with default reason - delete: calls DeleteActivityExecution - result: calls PollActivityExecution, outputs the activity result Shared helper buildStartActivityRequest() constructs the gRPC request from ActivityStartOptions, handling retry policy, ID policies, search attributes, headers, user metadata, and priority. Shared helper printActivityOutcome() formats activity results for both text and JSON output modes. Also adds description-header to temporal activity docs (required by code generator) and fixes import aliasing (common/v1 -> commonpb). Co-authored-by: Cursor <cursoragent@cursor.com>
Verify all 11 activity subcommands appear in help output (cancel, complete, count, delete, describe, execute, fail, list, result, start, terminate). Verify start command exposes expected flags (activity-id, type, task-queue, timeouts, input). Verify complete and fail commands expose both activity-reference options (activity-id, run-id) and the optional workflow-id flag. Integration tests for the new RPCs are deferred until a standalone-activity-enabled test server is available. Co-authored-by: Cursor <cursoragent@cursor.com>
- Use 'activity complete' as the primary example instead of experimental 'activity start' command - Restore original description-header text for docs - Revert unnecessary commonpb import alias (use default 'common' since no conflict exists in this file) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Activity count/list/result: use Standalone Activity Execution wording and 'only supported for Standalone Activity Execution' - Activity complete/fail: restore --workflow-id YourWorkflowId in examples - Keywords: restore 'activity execution' - Activity count/list: Search Attributes and queries; filter/to be counted - Workflow execute: add backslash after execute in example - Workflow start: revert to main (Initiate, Returns Workflow- and Run-IDs) - Workflow delete: revert to main (Remove, Executions typo); remove TODO - Workflow result: remove TODO (output verb already used) - Workflow count/list: example query and remove TODOs; queries. wording - Restore trailing spaces in worker deployment and task-queue docs to avoid whitespace-only diff noise Co-authored-by: Cursor <cursoragent@cursor.com>
This reverts commit 2ec455d.
- Activity complete/fail: restore --workflow-id in examples - Activity count/list: Standalone Activity Execution wording, filter/sentences, Search Attributes and queries - All standalone-only activity commands: summary + only supported sentence - Restore activity execution keyword - Workflow describe: revert summary to Show Workflow Execution info - Workflow delete: remove TODO - Workflow terminate: revert summary to Forcefully end a Workflow Execution - Workflow execute: add backslash after execute in example - Workflow count/list: remove TODO show an example query - Workflow result: remove TODO Co-authored-by: Cursor <cursoragent@cursor.com>
- Revert all trailing-whitespace-only changes in deployment/worker sections - Restore trailing whitespace on workflow headers line to match main - Apply workflow start description suggestion (mention workflow execute) - Apply id-reuse-policy description suggestion - Apply id-conflict-policy description suggestion - Add visibility docs link to search-attribute description Co-authored-by: Cursor <cursoragent@cursor.com>
Individual verb subcommands already have examples. Co-authored-by: Cursor <cursoragent@cursor.com>
The summaries already say "Standalone", making this sentence redundant in the descriptions. Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…ityExecution) Neither the Go nor Python SDK exposes this RPC, and the server does not support it yet.
The examples were missing the required timeout, which is confusing since either schedule-to-close-timeout or start-to-close-timeout must be provided. Use different timeouts in each example to illustrate both options.
The server returns an empty non-error PollActivityExecution response when the long-poll timeout (default 20s) expires before the activity completes. This is the server's signal to re-issue the poll. Both execute and result were making a single call and treating the nil outcome as a terminal error. Extract shared pollActivityOutcome that loops until an outcome arrives.
Verifies that pollActivityOutcome retries when the server returns an empty PollActivityExecution response (nil outcome), rather than treating it as a terminal error. Uses a gRPC interceptor to simulate the long-poll timeout on the first poll, then return a result on the second. Without the fix in 384741e, this test fails with "activity outcome not available".
- pollActivityOutcome: use `for resp.GetOutcome() == nil` loop matching the SDK-Go's PollActivityResult pattern - printActivityOutcome: use type switch on GetValue() matching SDK-Go style, with informative default case - Enable standalone activities in dev server (history.enableChasm, activity.enableStandalone) with short long-poll timeout (2s) - Rewrite long-poll retry test as proper integration test: activity sleeps 3s, exceeding the 2s long-poll timeout, forcing a retry
…mands Workflow command tests are 100% integration tests (SharedServerSuite), with no TestHelp_* unit tests. Conform to that precedent: - Remove TestHelp_ActivitySubcommands, TestHelp_ActivityStartFlags, TestHelp_ActivityCompleteFlags, TestHelp_ActivityFailFlags - Add integration tests: Start, Execute (success, failure, poll retry), Result, Describe, List, Count, Cancel, Terminate - Add startStandaloneActivity helper for tests that need a running standalone activity
… count/delete wording - Revert whitespace-only reformatting of `activity complete` description - Apply "Output a count" for `workflow count` per review suggestion - Apply "queries" for `workflow delete` per review suggestion
- Apply suggestion: parent description → "Perform operations on Activity Executions." - Replace "Temporal Markdown" with "standard Markdown excluding images, HTML, and script tags"
The test was asserting map[foo:bar] which is Go's internal format.
The expected output is JSON: {"foo":"bar"}.
printActivityOutcome now JSON-marshals values so complex types (maps, slices) render as JSON rather than Go's fmt representation.
Tests verify that `activity complete` and `activity fail` work for standalone activities (without --workflow-id, using --run-id).
Replace hand-rolled PollActivityExecution long poll loop with the SDK's client.GetActivityHandle().Get(), which provides proper gRPC long-poll handling and retry semantics. This is analogous to how workflow update uses UpdateWorkflow() + updateHandle.Get().
- Start output: add Type, Namespace, TaskQueue for parity with workflow start - Describe: switch from raw proto to SDK ActivityHandle.Describe() - Cancel: switch from raw proto to SDK ActivityHandle.Cancel() - Terminate: switch from raw proto to SDK ActivityHandle.Terminate() - Count: switch from raw proto to SDK cl.CountActivities() - List: add page size normalization matching workflow list - Add failing tests for execute JSON output (success + failure)
getActivityResult now produces structured JSON with activityId, runId, status (COMPLETED/FAILED), and result or failure details, matching the workflow execute JSON output pattern.
…tion proto Replace buildStartActivityRequest + raw proto call with cl.ExecuteActivity(ctx, opts, type, args...), mirroring the workflow pattern (buildStartOptions + cl.ExecuteWorkflow). The SDK now handles identity, request ID, input encoding, header propagation, user metadata serialization, and priority conversion. Removes the `started` field from start output since the SDK's ActivityHandle doesn't expose it (non-error return implies started).
The failure field should be a structured object with message, cause, etc. matching the workflow execute output, not a flat error string.
Use temporal.GetDefaultFailureConverter().ErrorToFailure() to convert the SDK error back to a proto Failure, then marshal it as JSON. This gives the same structured failure info (message, source, cause, applicationFailureInfo, activityFailureInfo) that workflow execute provides via its closeEvent.
Matches workflow count test coverage by verifying JSON output structure.
Text failure output should show Status and Failure message on stdout, matching the workflow execute pattern, not just log an error.
Use ErrorToFailure + MarshalFriendlyFailureBodyText to print a structured failure card (Status, Failure with Message/StackTrace/Cause) matching the workflow execute text output pattern.
Bring TestStandaloneActivity_Complete and _Fail into parity with the pre-existing workflow-bound tests by verifying results via handle.Get() and passing --identity.
Describe: add JSON output, --raw flag, and status/task-queue assertions. Count: already had JSON coverage from prior commit.
Cancel test now verifies RunState transitions to CancelRequested. Terminate test now verifies handle.Get() returns a termination error. Fix cancel YAML: use "Request cancellation" in summary; correct the claim that cancel has no effect on non-heartbeating activities (it does transition RunState to CancelRequested).
Match the workflow text output structure: start and execute now print a "Running execution:" section with ActivityId, RunId, Type, Namespace, TaskQueue. Execute additionally prints a "Results:" section with Status (COMPLETED/FAILED) and Result/Failure.
Merge JSON test into the success test (matching workflow pattern), verify activityId in JSON output, verify input was received by the activity.
The dev server supports GROUP BY for activity counts; the earlier attempt used the wrong column name (ActivityType).
Tests reproduce: - describe --raw producing identical output to non-raw - describe text showing ActivityType as raw proto JSON - describe text showing LastFailure as raw JSON blob - result on nonexistent activity returning FAILED instead of error - result -o json showing empty runId
- describe: non-raw text output now formats ActivityType as name, LastFailure as human-readable text, durations as Go durations. --raw preserves the raw proto output. - result: nonexistent activity returns a CLI error instead of misleadingly rendering as a FAILED result. - result: JSON output populates runId from describe when --run-id is not specified. - execute/result: --no-json-shorthand-payloads now works, showing raw payload format with metadata/encoding/data fields.
Use gRPC service directly instead of SDK wrapper to get proto response, matching workflow count behavior. Also fixes grouped text output to decode payloads via data converter.
| return getActivityResult(cctx, cl, c.ActivityId, c.RunId) | ||
| } | ||
|
|
||
| func buildStartActivityRequest( |
There was a problem hiding this comment.
🤖 Major: hand-rolling proto request instead of using Go SDK
The workflow counterpart uses the SDK: buildStartOptions() builds client.StartWorkflowOptions, then cl.ExecuteWorkflow() handles the proto construction, including search attributes, headers, user metadata, priority, and retry policy serialization.
Here, the entire StartActivityExecutionRequest proto is hand-built (~110 lines). The Go SDK provides client.StartActivityOptions with fields for all of these (ID, TaskQueue, timeouts, policies, RetryPolicy, TypedSearchAttributes, Summary, Details, Priority), and cl.ExecuteActivity(ctx, opts, activityType, args...) returns an ActivityHandle.
Specific issues with the hand-rolled approach:
-
Search attributes (line 157): uses
encodeMapToPayloadsto build rawcommon.SearchAttributes. The workflow version passesmap[string]anyto the SDK'sStartWorkflowOptions.SearchAttributes, letting the SDK serialize. Different code path, potential for subtle differences. -
UserMetadata (line 181): manually creates payloads with
json/plainencoding and%qformatting. The workflow version just setsStaticSummary/StaticDetailsas strings onStartWorkflowOptions, letting the SDK handle serialization. -
Headers (line 169): builds
common.Header{Fields: ...}directly on the proto. The workflow version usescontextWithHeaders()+ SDK interceptor pipeline. Since this call bypasses the SDK, headers won't flow through SDK interceptors.
One consideration: the SDK's ActivityHandle exposes GetID()/GetRunID() but not Started from the proto response. If Started is essential, you could call cl.ExecuteActivity() and always report started=true (since a non-error return implies start), or access it via the raw response if the SDK supports it.
See buildStartOptions in commands.workflow_exec.go:568 and startWorkflow at line 518 for the workflow pattern.
There was a problem hiding this comment.
I suspect you have a good point here but can you make it explicit what your actual proposal is?
There was a problem hiding this comment.
🤖 Proposal: Replace buildStartActivityRequest() + cl.WorkflowService().StartActivityExecution() with cl.ExecuteActivity(ctx, opts, activityType, args...).
Concrete mapping:
- CLI flags →
client.StartActivityOptionsfields (ID, TaskQueue, timeouts, RetryPolicy, TypedSearchAttributes, Summary, Details, Priority, ActivityIDConflictPolicy, ActivityIDReusePolicy) - Headers: use
contextWithHeaders()on the context, matching the workflow pattern instartWorkflow()— this routes through SDK interceptors correctly - Input: pass as varargs to
ExecuteActivityinstead of manually building payloads Startedfield: non-error return fromExecuteActivityimplies started=true
Benefits: eliminates ~110 lines of manual proto construction, gets search attribute encoding, user metadata serialization, and the interceptor pipeline for free.
One consideration: ExecuteActivity returns an ActivityHandle (not the raw proto response). We'd call handle.GetRunID() to get the run ID. I will tackle this in a follow-up commit.
There was a problem hiding this comment.
Sounds like a very good suggestion. Before you start, are there some bugs resulting from the fact that we're not doing this currently? For example, you mentioned interceptors. Should we have tests proving we can use interceptors? If so then we're missing those failing tests.
There was a problem hiding this comment.
🤖 Honest answer: I don't have evidence of concrete bugs from the current approach. The code paths differ but produce the same results in practice because:
-
Headers: The current code puts headers directly on the proto, bypassing `contextWithHeaders()` + `headerPropagator`. The headers still reach the server. The difference would only matter if an SDK interceptor needed to read/modify headers from context — the CLI doesn't register such interceptors.
-
Search attributes: `encodeMapToPayloads` is also used in the workflow start path (it builds raw `SearchAttributes` on the proto even for workflows). So both paths use the same encoding.
-
UserMetadata: The manual `json/plain` + `%q` encoding matches what the SDK does internally for `StaticSummary`/`StaticDetails`.
So the SDK migration is about consistency and future-proofing (ensuring interceptors apply if they're ever added), not fixing current bugs. I would not recommend writing failing tests for interceptor behavior since the CLI doesn't register interceptors — there's nothing to test against.
The stronger argument for this migration is reducing code: ~110 lines of manual proto construction replaced by SDK option mapping. It also means any future SDK improvements (e.g. new fields, better encoding) automatically apply.
There was a problem hiding this comment.
OK, please go ahead and make the change.
There was a problem hiding this comment.
🤖 Done. Commit a8b4a8a replaces buildStartActivityRequest + raw proto with cl.ExecuteActivity(ctx, opts, type, args...), mirroring the workflow pattern.
Key changes:
- New
buildStartActivityOptions→client.StartActivityOptions(analogous to workflow'sbuildStartOptions) - New
startActivityhelper: builds options, builds input viabuildRawInput(), sets headers viacontextWithHeaders(), callscl.ExecuteActivity() - SDK handles identity, request ID, input encoding, header propagation via context propagators, user metadata serialization, and priority conversion
- Search attributes: converted from
map[string]anytoTypedSearchAttributesvia newmapToSearchAttributeshelper (infers type from Go value) - Removed
startedfield from start output (non-error return from SDK implies started) - Net reduction: ~25 lines (-97/+71)
All 15 standalone activity tests pass.
| return req, nil | ||
| } | ||
|
|
||
| func getActivityResult(cctx *CommandContext, cl client.Client, activityID, runID string) error { |
There was a problem hiding this comment.
🤖 Result output less informative than workflow counterpart
Compare with the workflow's printTextResult (commands.workflow_exec.go:470) and workflowJSONResult (commands.workflow_exec.go:390):
-
Workflow text output: colored Status (COMPLETED/FAILED/TIMEOUT/CANCELED), RunTime, Result with encoding, and Failure details — all via
cctx.Printer.PrintStructured. -
Activity text output: just
Result: %swith raw JSON bytes. -
Workflow JSON output: rich struct with workflowId, runId, type, namespace, status, closeEvent, result, and optional history.
-
Activity JSON output:
{"result": ...}— minimal.
For consistency with the workflow experience, consider printing a status and using PrintStructured for text output.
There was a problem hiding this comment.
Would you agree that there should be a failing test here if our JSON output is inadequate?
There was a problem hiding this comment.
🤖 Yes, agreed. I've now added failing tests that covered exactly this gap — TestStandaloneActivity_Execute_Success_JSON and TestStandaloneActivity_Execute_Failure_JSON. Both were failing because getActivityResult produced no status field (success case) and no JSON at all (failure case). The fix in the subsequent commit adds structured JSON output with activityId, runId, status, and result/failure fields.
internal/temporalcli/commands.yaml
Outdated
| - Temporal CLI | ||
|
|
||
| - name: temporal activity cancel | ||
| summary: Cancel a Standalone Activity Execution (Experimental) |
There was a problem hiding this comment.
| summary: Cancel a Standalone Activity Execution (Experimental) | |
| summary: Request cancellation of a Standalone Activity (Experimental) |
The verbal form should be "Request cancellation" whenever referring to activity cancel in prose. It's not pointless verbosity: the request is delivered to the activity execution and can be swallowed, preventing cancellation.
There was a problem hiding this comment.
🤖 Fixed (eb72426). Summary now reads "Request cancellation of a Standalone Activity Execution" and all prose uses "Request cancellation" / "Requesting cancellation".
internal/temporalcli/commands.yaml
Outdated
| error will be raised when the next heartbeat response is | ||
| received; if the Activity allows this error to propagate, the | ||
| Activity transitions to canceled status. If the Activity is | ||
| not heartbeating, this request has no effect on the Activity. |
There was a problem hiding this comment.
If the Activity is not heartbeating, this request has no effect on the Activity.
Double check this please. Consult tests in the server repo (temporal). I believe the activity enters a special CancelRequested state. Is it really true that, if the activity is not heartbeating, the behavior is indistinguishable from if the cancellation request had not been sent?
There was a problem hiding this comment.
🤖 Good catch — that claim was wrong. The server tests confirm that cancel transitions the activity's RunState to PENDING_ACTIVITY_STATE_CANCEL_REQUESTED regardless of heartbeating (standalone_activity_test.go:1211-1214). The state change is visible via Describe.
Fixed (eb72426): removed the incorrect sentence and replaced with: "Requesting cancellation transitions the Activity's run state to CancelRequested. If the Activity is heartbeating, a cancellation error will be raised when the next heartbeat response is received; if the Activity allows this error to propagate, the Activity transitions to canceled status."
internal/temporalcli/commands.yaml
Outdated
|
|
||
| - name: temporal activity complete | ||
| summary: Complete an Activity | ||
| summary: Complete an Activity with a result |
There was a problem hiding this comment.
We wish to make the summaries explanatory rather than merely echoing the command name.
| summary: Complete an Activity with a result | |
| summary: Mark an activity as successfully finished with a result |
There was a problem hiding this comment.
🤖 Done (77aa23e). Applied the suggested summary.
b24097a to
51516a6
Compare
- Remove the describe call in getActivityResult that resolved run ID when --run-id was omitted. This avoids an unexpected extra RPC, matching the workflow result command's behavior. - Replace non-standard CONSIDER(dan) comment with factual note about SDK cancel/terminate reason defaults.
mapToSearchAttributes sends all string values as Keyword, even when the server schema defines the attribute as Datetime. This causes the value to be silently stored with the wrong type, making it invisible to visibility queries. The Int case (Float64 vs Int64) is handled by server coercion.
The SQLite dev server has a format mismatch between the STRFTIME generated column and the Go query converter for whole-second datetime values. Keep the test body so it can be unskipped once the server bug is fixed.
Analogous to SDK test "Polling does not cease prematurely". Activity sleeps 12s (past the 10s gRPC defaultRPCTimeout), server returns empty poll responses every 2s, CLI must keep retrying without timing out.
Match the SDK's pollActivityTimeout: each PollActivityExecution call gets a fresh 60s context, preventing indefinite hangs on unresponsive servers. If the per-request context expires but the parent context is still alive, retry. Real server errors (e.g. NotFound) propagate immediately.
Summary
Test plan